package districting;

import java.util.*;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.operation.polygonize.Polygonizer;

public class ShortestBisectingLineRegion extends Zone {

   private static final int RESOLUTION_OF_ANGLE = 360;

   private static final int EXTENSION_FACTOR = 1000;

   private Double angleOfBisector;

   private int cutOffPopulation;
   
   private List<PollingBooth> boothsList;

   public ShortestBisectingLineRegion(int numDivisions) {
      this.numDivisions = numDivisions;
      booths = new HashSet<PollingBooth>();
      boothsList = new ArrayList<PollingBooth>();
      votingPopulation = 0;
      divisions = new HashMap<DivisionMethod, List<Division>>();
      divisions.put(DivisionMethod.SHORTEST_BISECTING_LINE,
            new ArrayList<Division>());
      pbGeometry = new GeometryFactory().createMultiPoint(new Coordinate[] {});
   }
   
   public void addPB(PollingBooth pb){
      super.addPB(pb);
      boothsList.add(pb);
   }

   public void createFromState(State state) {
      for (PollingBooth pb : state.getBooths()) {
         addPB(pb);
      }

      for (int i = 0; i < numDivisions; i++) {
         Division division = new Division(state.getName() + Integer.toString(i)
               + "_SBLM", state.getId() * 100 + i,
               DivisionMethod.SHORTEST_BISECTING_LINE);
         addDivision(division, DivisionMethod.SHORTEST_BISECTING_LINE);
         state.addDivision(division, DivisionMethod.SHORTEST_BISECTING_LINE);
      }
      this.area = state.getArea();
   }

   private void calculateCutOffPopulation() {
      cutOffPopulation = votingPopulation * (numDivisions / 2) / numDivisions;
   }

   public void bisectRegion() {
      if (numDivisions == 1) {
         finaliseRegion();
      } else {
//         System.out.println("Region.bR");
//         System.out.println(area);
         calculateCutOffPopulation();

         Geometry shortestBisectingLine = shortestBisectingLine();

         PBAngleComparator angleComparator = new PBAngleComparator(
               angleOfBisector);
         Collections.sort(boothsList, angleComparator);

         ShortestBisectingLineRegion region1 = new ShortestBisectingLineRegion(numDivisions / 2);
         ShortestBisectingLineRegion region2 = new ShortestBisectingLineRegion(numDivisions - numDivisions / 2);

         int i = 0;
         for (Division division : getDivisions(DivisionMethod.SHORTEST_BISECTING_LINE)) {
            if (i < numDivisions / 2) {
               region1.addDivision(division,
                     DivisionMethod.SHORTEST_BISECTING_LINE);
            } else {
               region2.addDivision(division,
                     DivisionMethod.SHORTEST_BISECTING_LINE);
            }
            i++;
         }

         region1.checkNumDivisions(DivisionMethod.SHORTEST_BISECTING_LINE);
         region2.checkNumDivisions(DivisionMethod.SHORTEST_BISECTING_LINE);

         int cumulativePopulation = 0;
         for (PollingBooth pb : boothsList) {
            if (cumulativePopulation < cutOffPopulation) {
               region1.addPB(pb);
            } else {
               region2.addPB(pb);
            }
            cumulativePopulation += pb.getTotalVotes();
         }

         // Calculate areas.
         Geometry rightHalfArea = generateHalfArea(shortestBisectingLine, 1);
         Geometry leftHalfArea = generateHalfArea(shortestBisectingLine, -1);

         region1.setArea(rightHalfArea);
         region2.setArea(leftHalfArea);

         region1.bisectRegion();
         region2.bisectRegion();
      }

   }

   // 1 means take the right hand side (using the angle of bisector)
   // -1 means take the left hand side
   private Geometry generateHalfArea(Geometry shortestBisectingLine, int side) {
      CoordinateAngleComparator coordinateAngleComparator = new CoordinateAngleComparator(
            angleOfBisector);

      Geometry halfArea = new GeometryFactory().createMultiPolygon(null);

      Polygonizer polygonizer = new Polygonizer();
      polygonizer.add(area.getEnvelope().getBoundary().union(
            shortestBisectingLine));

      Collection<Polygon> halfPlanes = polygonizer.getPolygons();

      for (Polygon halfPlane : halfPlanes) {
//         System.out.println("Region.gHP");
//         System.out.println(halfPlane);
         if (coordinateAngleComparator.compare(shortestBisectingLine
               .getCentroid().getCoordinate(), halfPlane.getCentroid()
               .getCoordinate()) == side) {
            halfArea = halfPlane.intersection(area);
         }
      }

      if (halfArea == null) {
         System.out.println("Region.gHB oops");
         System.out.println(area);
      }
      return halfArea;

   }

   private void finaliseRegion() {
      Division division = getDivisions(DivisionMethod.SHORTEST_BISECTING_LINE)
            .get(0);
      for (PollingBooth pb : booths) {
         pb.setDivision(division, DivisionMethod.SHORTEST_BISECTING_LINE);
         division.addPB(pb);
      }
      division.setArea(area);
//      System.out.println("Region.fR division number: " + division.getId());
//      System.out.println(area);
   }

   private Geometry shortestBisectingLine() {
      Double bestAngle = 0.0;
      Geometry shortestLine = null;
      Double bestLength = Double.MAX_VALUE;
      for (Double angle = 0.0; angle < 2 * Math.PI; angle += 2 * Math.PI
            / RESOLUTION_OF_ANGLE) {
         PBAngleComparator angleComparator = new PBAngleComparator(angle);
         Collections.sort(boothsList, angleComparator);

         Geometry bisectingLine = null;
         int cumulativePopulation = 0;
         double x1 = 0;
         double x2 = 0;
         double y1 = 0;
         double y2 = 0;
         for (PollingBooth pb : boothsList) {
            if (cumulativePopulation > cutOffPopulation) {
               x1 = pb.getPoint().getX();
               y1 = pb.getPoint().getY();
               bisectingLine = new GeometryFactory()
                     .createLineString(new Coordinate[] {
                           new Coordinate((x1 + x2) / 2 - EXTENSION_FACTOR
                                 * Math.cos(angle), (y1 + y2) / 2
                                 - EXTENSION_FACTOR * Math.sin(angle)),
                           new Coordinate((x1 + x2) / 2 + EXTENSION_FACTOR
                                 * Math.cos(angle), (y1 + y2) / 2
                                 + EXTENSION_FACTOR * Math.sin(angle)) });
               break;
            }
            x2 = pb.getPoint().getX();
            y2 = pb.getPoint().getY();
            cumulativePopulation += pb.getTotalVotes();
         }

         Geometry bisectingInterval = bisectingLine.intersection(area);

         double length = bisectingInterval.convexHull().getLength();

         if (length < bestLength) {
            bestLength = length;
            bestAngle = angle;
            shortestLine = bisectingLine;
         }

      }
      angleOfBisector = bestAngle;
      return shortestLine;
   }
}
